﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using NATUPNPLib;

namespace GShare1._0.Client
{
    public delegate void UiThreadInvoker();
    public delegate void MainThreadInvoker(MethodInvoker del);
    public delegate void MessageBoxInvoker(string msg);
    public class ClientConnection
    {
        //private variables
        private IPAddress _serverIp;
        private int _port;
        private AutoResetEvent _autoReset;
        private AutoResetEvent _downloadReady;
        private Upload _download = null;
        private bool _connected;
        private List<ClientInfo> clientInfoList = new List<ClientInfo>();
        private string _username;
        private MainThreadInvoker _mainThreadInvoker;
        private UiThreadInvoker _invoker;
        private MessageBoxInvoker _mboxInvoker;
        private byte[] _buffer;
        private Socket _clientSocket;
        private Socket _updateSocket;
        private FileManager _fileManager;
        private ClientListUpdateDelegate _listInvoker;
        private UPnPNATClass nat;

        public ClientListUpdateDelegate ListInvoker
        {
            get { return _listInvoker; }
            set { _listInvoker = value; }
        }


        #region props

        public Upload Download
        {
            get { return _download; }
            set { _download = value; }
        }
        public FileManager FileManager
        {
            get { return _fileManager; }
            set { _fileManager = value; }
        }
        public byte[] Buffer
        {
            get { return _buffer; }
            set { _buffer = value; }
        }

        public Socket Socket
        {
            get { return _clientSocket; }
            set { _clientSocket = value; }
        }

        public string Username
        {
            get { return _username; }
            set { _username = value; }
        }

        public MessageBoxInvoker MessageInvoker
        {
            get { return _mboxInvoker; }
            set { _mboxInvoker = value; }
        }

        public UiThreadInvoker Invoker
        {
            get { return _invoker; }
            set { _invoker = value; }
        }

        public MainThreadInvoker MainThreadInvoker
        {
            get { return _mainThreadInvoker; }
            set { _mainThreadInvoker = value; }
        }

        public bool Connected
        {
            get { return _connected; }
            set { _connected = value; }
        }
        #endregion

        public ClientConnection(IPAddress ip, int port, AutoResetEvent autoReset, string username)
        {
            _username = username;
            _port = port;
            _serverIp = ip;
            _connected = false;
            _autoReset = autoReset;
            _fileManager = new FileManager();
            nat = new UPnPNATClass();
        }


        /// <summary>
        /// Tries to connect to the socket using BeginConnect Asynchronusly
        /// </summary>
        internal void Connect()
        {
            try
            {
                //CreateFolders
                _fileManager.CheckContainingFolder();

                //create port forwarding
                //IStaticPortMappingCollection mappings = nat.StaticPortMappingCollection;
                //mappings.Add(3333, "TCP", 3333, IpAddressHelper.LocalIpAddress(), true, "Gshare Client");

                _clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                //_clientSocket.BeginConnect(_serverIp, _port, new AsyncCallback(ConnectCallback), null);


                // Connect using a timeout (5 seconds)

                IAsyncResult result = _clientSocket.BeginConnect(_serverIp, _port, null, null);

                bool success = result.AsyncWaitHandle.WaitOne(5000, true);

                if (!success)
                {


                }
                else
                {

                    ConnectCallback(result);
                    //wait 5 secs to see if we connect sucsessfuly
                    //_autoReset.WaitOne(5000);


                }

            }
            catch (Exception)
            {

                throw;
            }
        }

        /// <summary>
        /// When the Client Connects to the socket Successfuly this is called (Callback for BeginConnect)
        /// Sends Buffer with login info to server
        /// </summary>
        /// <param name="AR">IAsyncResult from the BeginConnect</param>
        private void ConnectCallback(IAsyncResult AR)
        {
            try
            {

                _buffer = Encoding.ASCII.GetBytes("CONNECT" + "," + _username);
                //send login info to server

                if (_clientSocket.Connected)
                {
                    _clientSocket.Send(_buffer);
                    //Buffer = Encoding.ASCII.GetBytes("Lol");
                    //_clientSocket.Send(Buffer);

                    //reset the buffer for another use
                    _buffer = new byte[_clientSocket.ReceiveBufferSize];
                    _clientSocket.EndConnect(AR);
                    int received = _clientSocket.Receive(_buffer);
                    Array.Resize(ref _buffer, received);
                    string msg = Encoding.ASCII.GetString(_buffer);
                    string[] msgSplit = msg.Split(',');
                    switch (msgSplit[0])
                    {
                        case "CONNECT_SUCCESS":
                            _connected = true;

                            _buffer = new byte[_clientSocket.ReceiveBufferSize];
                            _clientSocket.BeginReceive(_buffer, 0, _clientSocket.ReceiveBufferSize, SocketFlags.None, new AsyncCallback(ReceiveCallback), null);
                            _autoReset.Set();
                            break;
                        case "CONNECT_FAIL":
                            if (msgSplit[1] == "USERNAME_TAKEN")
                            {
                                MessageBox.Show("Username: " + _username + " has alreadly been taken");
                                _connected = false;
                                _invoker.BeginInvoke(null, null);
                                return;
                            }
                            break;
                        default:
                            _connected = false;
                            _invoker.BeginInvoke(null, null);
                            break;
                    }
                }
                else
                {
                    _connected = false;
                    _invoker.BeginInvoke(null, null);
                    return;
                }

            }
            catch (Exception ex)
            {

                MessageBox.Show(ex.Message);
            }
        }

        internal void ReceiveCallback(IAsyncResult AR)
        {
            try
            {

                //INSERT SOME HANDELING FOR SERVER GOING OFFLINE ATM ITS CAUGHT HERE
                if (_clientSocket.Connected)
                {
                    int received = _clientSocket.EndReceive(AR);
                    //Check if still connected
                    if (received != 0)
                    {
                        Array.Resize(ref _buffer, received);

                        string msg = Encoding.ASCII.GetString(_buffer);

                        switch (msg)
                        {
                            case "COMMANDERROR":
                                //End the active download
                                if (_download.Alive)
                                {
                                    _download.ServerReceiving = false;
                                    _mboxInvoker.BeginInvoke("Upload Stopped", null, null);
                                }
                                break;
                            case "CLIENT_INFO_LIST_UPDATE":

                                ClientInfoUpdateReceiver();
                                break;
                            default:
                                MessageReceiver.ReadMessage(msg, _mboxInvoker);
                                break;
                        }

                        //Restart Receive Async Loop
                        _buffer = new byte[_clientSocket.ReceiveBufferSize];
                        //common problem where server goes off line during normal functioning
                        try
                        {
                            _clientSocket.BeginReceive(_buffer, 0, _buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), null);
                        }
                        catch (Exception)
                        {
                        }
                    }
                    else
                    {
                        //We have Disconnected from the server now
                        //Display Message
                        //Close Form
                        //Set Connected = false;
                        //End Loop
                        MessageBox.Show("Disconnected From Server");
                        Invoker.BeginInvoke(null, null);
                        _connected = false;
                        return;
                    }

                }
                else
                {
                }

            }
            catch (Exception ex)
            {

            }

        }

        internal void Disconnect()
        {
            try
            {
                clientInfoList.Clear();
                _clientSocket.Shutdown(SocketShutdown.Both);
                _clientSocket.Dispose();
                _clientSocket.Close();
                IStaticPortMappingCollection mappings = nat.StaticPortMappingCollection;
                mappings.Remove(3333,"Gshare Client");
                
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

        /// <summary>
        /// sends message to the server
        /// </summary>
        /// <param name="messageType">Type of Message that the server will use to determin what to do with the body</param>
        /// <param name="message">the body of the message if needed</param>
        internal void SendMessage(string messageType, string message = null)
        {
            try
            {

                if (!string.IsNullOrEmpty(messageType))
                {
                    byte[] sendBuffer = Encoding.ASCII.GetBytes(messageType);
                    _clientSocket.BeginSend(sendBuffer, 0, sendBuffer.Length, SocketFlags.None, new AsyncCallback(SendCallback), null);
                    _autoReset.WaitOne();
                }
                if (message != null)
                {
                    byte[] sendBuffer = Encoding.ASCII.GetBytes(message);
                    _clientSocket.BeginSend(sendBuffer, 0, sendBuffer.Length, SocketFlags.None, new AsyncCallback(SendCallback), null);
                    _autoReset.WaitOne();
                }




            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

        private void SendCallback(IAsyncResult ar)
        {
            _autoReset.Set();
        }

        internal void UploadFile()
        {
            _downloadReady = new AutoResetEvent(false);
            OpenFileDialog ofd = new OpenFileDialog();
            //Stops user selecting shortcut - HACKY
            ofd.FileOk += ofd_FileOk;

            if (ofd.ShowDialog() == DialogResult.OK)
            {
                FileInfo FI = new FileInfo(ofd.FileName);

                //Check if file size is 0
                if (FI.Length == 0)
                {
                    MessageBox.Show("Cannot Select File With Size of 0 bytes");
                    return;
                }
                _autoReset = new AutoResetEvent(false);



                _download = new Client.Upload(ofd.FileName, (int)FI.Length);

                _download.ClientConnection = this;
                _download.StartConnection(_serverIp);
                _download.Finish += download_Finish;


            }
        }

        /// <summary>
        /// //Does not allow user to select a shortcut
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void ofd_FileOk(object sender, System.ComponentModel.CancelEventArgs e)
        {
            OpenFileDialog obj = (OpenFileDialog)sender;
            string ext = Path.GetExtension(obj.FileName);
            if (String.Compare(ext, ".url", true) == 0) e.Cancel = true;

        }

        void download_Finish(object sender, EventArgs e)
        {
            _download = null;
        }

        internal void ClientInfoUpdateReceiver()
        {
            clientInfoList.Clear();
            _updateSocket = new System.Net.Sockets.Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
            _updateSocket.BeginConnect(_serverIp, 3333, new AsyncCallback(UpdateUsersConnectCallback), null);
        }
        private void UpdateUsersConnectCallback(IAsyncResult ar)
        {
            byte[] buffer = Encoding.ASCII.GetBytes("UPDATELIST" + "," + Username);
            _updateSocket.Send(buffer);
            _updateSocket.EndConnect(ar);


            ClientInfoState state = new ClientInfoState();
            state.Buffer = new byte[_updateSocket.ReceiveBufferSize];
            //Start Listening for the data from the server 
            _updateSocket.BeginReceive(state.Buffer,0,state.Buffer.Length, SocketFlags.None, new AsyncCallback(BeginReceiveUpdateCallback), state);
        }

        

        private void BeginReceiveUpdateCallback(IAsyncResult ar)
        {

            ClientInfoState state = (ClientInfoState)ar.AsyncState;
            int received = _updateSocket.EndReceive(ar);
            //check if connected and data recieved
            if (_updateSocket.Connected && received != 0)
            {
                //check if first time receiving
                if (state.DataReceived == 0)
                {
                    state.NumberOfClientToReceive = BitConverter.ToInt32(state.Buffer, state.Offset);
                    state.Offset += 4;
                    state.DataSize = BitConverter.ToInt32(state.Buffer, state.Offset);
                    state.Offset += 4;
                }
                state.DataReceived += received;
                while(state.NumberOfClientToReceive != state.NumberOfClientToReceived)
                {
                    int objectSize = BitConverter.ToInt32(state.Buffer, state.Offset);
                    state.Offset += 4;

                    using (MemoryStream stream = new MemoryStream(state.Buffer, state.Offset, objectSize))
                    {
                        BinaryFormatter BF = new BinaryFormatter();
                        ClientInfo clientInfo = (ClientInfo)BF.Deserialize(stream);
                        state.NumberOfClientToReceived++;
                        clientInfoList.Add(clientInfo);
                    }

                    state.Offset += objectSize;
                    

                    //check if we have all data
                    if (state.DataSize != state.DataReceived)
                    {
                        _updateSocket.BeginReceive(state.Buffer, 0, state.Buffer.Length, SocketFlags.None, new AsyncCallback(BeginReceiveUpdateCallback), state);
                        return;
                    }
                }
                    _listInvoker.BeginInvoke(clientInfoList, null, null);
                //start the receive loop again for more data to be received forthe next msg
                    //_updateSocket.BeginReceive(state.Buffer, 0, state.Buffer.Length, SocketFlags.None, new AsyncCallback(BeginReceiveUpdateCallback), state);
                    _updateSocket.Close();
                
                    
                    
            }
        }


    }
}



